const classPartAttrs = ['px-', 'py-', 'p-', 'mx-', 'my-', 'm-']
  .map(it => [5, 10, 15, 20, 25].map(jt => `${it}${jt}`))
  .flat()
const colorAttrs = [
  'pink', 'red', 'teal', 'green', 'blue', 'yellow', 'orange', 'purple', 'white', 'gray', 'black'
]

const classAttrs = colorAttrs.concat(colorAttrs.map(it => `bg-${it}`))
  .concat(colorAttrs.map(it => `box-${it}`))
  .concat(colorAttrs.map(it => `box-dotted-${it}`))
  .concat(colorAttrs.map(it => `box-dashed-${it}`))
  .concat(classPartAttrs)


const allowTags = [
  'span', 'b', 's', 'u', 'mark', 'i', 'a', 'img',
  'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
  'div', 'p', 'blockquote',
  'ol', 'ul', 'li',
  'dl', 'dt', 'dd',
  'table', 'caption', 'thead', 'tbody', 'tr', 'th', 'td',
  'hr', 'br',
  'f', 'fb',
  'code', 'script',
].concat(classAttrs)

const inlineTags = [
  'span', 'b', 's', 'u', 'mark', 'i', 'a',
  'f',
  'red', 'px-5'
]

export function isClassAttr(key: string): boolean {
  return classAttrs.indexOf(key) !== -1// || /^(px|py|p|mx|my|m)-(5|10|15|20)$/.test(key)
}

export function transformAttrs(attrs?: Record<string, string>): Record<string, string> {
  if (!attrs || !Object.keys(attrs)) return {}
  for (const key in attrs) {
    const val = attrs[key]
    if (key === 'align' && (val === 'left' || val === 'center' || val === 'right')) {
      delete attrs[key]
      if (attrs.style) {
        if (attrs.style[attrs.style.length - 1] !== ';') {
          attrs.style += ';'
        }
        attrs.style += `text-align:${val};`
      } else {
        attrs.style = `text-align:${val};`
      }
    } else {
      for (const classAttr of classAttrs) {
        if (key === classAttr) {
          delete attrs[key]
          if (attrs.class) {
            attrs.class += ` ${classAttr}`
          } else {
            attrs.class = classAttr
          }
          break
        }
      }
    }
  }
  return attrs
}

export function joinAttrs(attrs?: Record<string, string>) {
  if (!attrs) return undefined
  return Object.entries(attrs).map(([key, value]) => {
    if (value != null) {
      return `${key}="${value}"`
    } else {
      return `${key}`
    }
  }).join(' ')
}

export class YedongHtmlParser {
  input: string;
  len: number;
  cur: number;
  tags: { tag: string, level: number }[]; // tag堆栈，必须封闭
  tagLevel: number;
  line: number;
  col: number;

  constructor() {
    this.input = '';
    this.len = 0;
    this.cur = 0;
    this.tags = [];
    this.tagLevel = 0;
    this.line = 1;
    this.col = 0;
  }

  get eof() {
    return this.cur >= this.len;
  }

  get pos() {
    return {line: this.line, col: this.col}
  }

  move(length: number) {
    this.cur += length
  }

  isEof(length = 0) {
    return this.cur + length >= this.len;
  }

  look(offset = 0) {
    return this.input[this.cur + offset];
  }

  peek() {
    const ch = this.input[this.cur++];
    switch (ch) {
      case '\n':
        this.col = 0;
        this.line++;
        break;
      case '\t':
        this.col += 4;
        break;
      default:
        this.col++;
        break;
    }
    return ch
  }

  accept(c: string) {
    const curChar = this.look();
    if (curChar === c) {
      this.peek()
    } else {
      console.log(`Unexpected character(${this.line}): '${curChar}' should be '${c}'`);
      return false
    }
    return true;
  }

  consume(offset: number, length: number): string | null {
    if (length === 0) return null
    const start = Math.min(this.cur + offset, this.len - 1)
    const end = Math.min(this.cur + offset + length, this.len)
    for (let i = start; i < end; i++) {
      this.peek()
    }
    return this.input.substring(start, end);
  }

  // 多个连续空格当一个空格，多个连续换行当一个 <br/>
  isSpace(c: string) {return c === '\n' || c === '\r' || c === ' ' || c === '\t'}

  consumeSpace() {
    do {
      const curChar = this.look();
      if (this.isSpace(curChar)) {
        this.peek()
      } else {
        break;
      }
    } while (!this.eof);
  }

  tryMathStr(endStr: string, offset: number = 0) {
    let length = this.cur + offset;
    while ((length + endStr.length <= this.len) && !this.input.startsWith(endStr, length)) {
      length++
    }
    return this.consume(offset, length - this.cur - offset)
  }

  tryMathEndTag(offset: number = 0) {
    let length = offset
    while ((this.cur + length + 4 <= this.len) && !this.isTagEnd(length).len) {
      length++
    }
    return this.consume(offset, length - offset)
  }

  consumeWhile(regex: RegExp) {
    const result: string[] = [];
    do {
      const curChar = this.look();
      if (regex.test(curChar)) {
        result.push(curChar);
        this.peek()
      } else {
        break;
      }
    } while (!this.eof);
    return result.join('');
  }

  private isNameFirst(c: string) {
    // 首字符为 [a-zA-Z\-_]
    return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || '-' === c || '_' === c;
  }

  private isNamePart(c: string) {
    // 中间字符为 [a-zA-Z0-9\-_]
    return ('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || '-' === c || '_' === c;
  }

  lookName(offset = 0): { len: number, name: string } {
    let length = offset
    let curChar = this.look(length);
    if (this.isNameFirst(curChar)) {
      length++
      do {
        curChar = this.look(length);
        if (this.isNamePart(curChar)) {
          length++;
        } else {
          break;
        }
      } while (!this.isEof(length));
    }

    return {name: this.input.substring(this.cur + offset, this.cur + length), len: length - offset}
  }

  isTagStart(offset = 0): { len: number, name: string } {
    let curChar = this.look(offset)
    if (curChar !== '<') return {len: 0, name: ''}
    const res = this.lookName(offset + 1)
    const nextChar = this.look(offset + res.len + 1)
    if ((this.isSpace(nextChar) || nextChar === '/' || nextChar === '>') && allowTags.indexOf(res.name) >= 0) {
      return res
    } else {
      return {len: 0, name: ''}
    }
  }

  isTagEnd(offset = 0): { len: number, name: string } {
    if (this.look(offset) !== '<') return {len: 0, name: ''}
    if (this.look(offset + 1) !== '/') return {len: 0, name: ''}
    const res = this.lookName(offset + 2)
    const nextChar = this.look(offset + res.len + 2)
    if (nextChar === '>' && allowTags.indexOf(res.name) >= 0) {
      return res
    } else {
      return {len: 0, name: ''}
    }
  }

  parse(input = '', options = {}) {
    this.input = input;
    this.len = input.length;
    this.cur = 0;
    this.tags = [];
    this.tagLevel = 0;
    return this.parseNodes();
  }

  parseNodes(matchTag = '') {
    const oldTagLevel = this.tagLevel++
    const nodes: any[] = [];
    do {
      let curChar = this.look()
      const isTagStart = this.isTagStart()
      const isTagEnd = this.isTagEnd()
      if ((this.tagLevel > 1 || inlineTags.indexOf(isTagStart.name) === -1) && isTagStart.len > 0 || isTagEnd.len > 0) {
        if (isTagStart.len > 0) {
          const element = this.parseElement(matchTag)
          if (element) nodes.push(element);
        } else {
          break
        }
      } else {
        // 顶级文本，断行后，加 p 标签
        if (this.tagLevel === 1) {
          const line = this.line
          const children = this.parseParaNodes()
          if (children?.length) {
            nodes.push({tag: 'p', attrs: {}, line, children});
          }
        } else {
          const children = this.parseTextNodes()
          if (children?.length) {
            nodes.push(...children);
          }
        }
      }
    } while (!this.eof);
    this.tagLevel = oldTagLevel
    return nodes;
  }

  /**
   * 1) 顶级文本，加 p 标签
   * 1) 忽略头部和尾部空行
   * 2) 中间空行表示本行段落结束
   * "\n" 替换为 <br/>
   * "    \n" 空行替换为 <p>...</>
   */
  parseParaNodes() {
    const nodes: any[] = [];
    let chars: string[] = []
    let length = 0
    this.consumeSpace()

    const resolveText = () => {
      if (length > 0) {
        this.consume(0, length)
        length = 0
      }
      if (chars.length > 0) {
        // 消耗 length 长度字符
        nodes.push({tag: 'text', props: {text: chars.join('')}})
        chars = []
      }
    }

    do {
      let curChar = this.look(length);
      // 遇到连续的空白
      let currLength = length
      let newLineCount = 0
      while (this.isSpace(curChar)) {
        if (curChar === '\n') newLineCount++
        curChar = this.look(++length);
      }
      if (currLength !== length || curChar == null) {
        if (newLineCount > 0) {
          // 如果里面有回车，就插入 1）之前匹配的字符 和 2）换行 <br/>
          resolveText()
          if (newLineCount > 1) {
            // 多个换行，就结束 p 标签
            return nodes
          } else {
            nodes.push({tag: 'br'})
          }
        } else {
          // 仅仅是多个空白字符，用空格代替之
          chars.push(' ')
        }
      }

      // 子标签
      if (curChar === '<') {
        // 如果 < 后面紧跟 tag，代表匹配上 Element
        const isTagStart = this.isTagStart(length)
        if (isTagStart.len > 0) {
          // if (this.isNameFirst(this.look(length + 1))) {
          //   const {name} = this.lookName(length + 1)
          if (inlineTags.indexOf(isTagStart.name) !== -1) {
            resolveText()
            const element = this.parseElement()
            if (element) nodes.push(element)
          } else {
            // 块级元素
            resolveText()
            return nodes
          }
        } else {
          chars.push(curChar)
          length++;
        }
      } else if (curChar === '$') {
        const res = this.tryParseLatex(length);
        const matchBegin = Math.min(this.cur + length, this.len)
        const matchEnd = Math.min(this.cur + res.matchEnd, this.len)
        if (res.node == null) {
          // 没有匹配上，就向前移动 matchEnd - length + 1 个字符
          chars = chars.concat(this.input.substring(matchBegin, matchEnd))
          length = matchEnd
        } else {
          // 碰到 Latex 公式
          resolveText()
          nodes.push(res.node)
          this.consume(length, res.matchEnd - length)
          length = 0
        }
      } else {
        chars.push(curChar)
        length++;
      }
    } while (!this.isEof(length));

    resolveText()
    return nodes
  }

  parseTextNodes() {
    const nodes: any[] = [];
    let chars: string[] = []
    let length = 0
    let hasNewLine = true // 当前匹配的是空行
    this.consumeSpace()

    const resolveText = () => {
      if (length > 0) {
        this.consume(0, length)
        length = 0
      }
      if (chars.length > 0) {
        // 消耗 length 长度字符
        nodes.push({tag: 'text', props: {text: chars.join('')}})
        chars = []
      }
    }

    do {
      let curChar = this.look(length);
      // 遇到连续的空白
      let currLength = length
      while (this.isSpace(curChar)) {
        hasNewLine = curChar === '\n'
        length++
        curChar = this.look(length);
      }
      if (currLength !== length) {
        if (hasNewLine) {
          // 如果里面有回车，就插入 1）之前匹配的字符 和 2）换行 <br/>
          resolveText()
          nodes.push({tag: 'br'})
        } else {
          // 仅仅是多个空白字符，用空格代替之
          chars.push(' ')
        }
      }
      // 子标签
      if (curChar === '<') {
        // 如果 < 后面紧跟 tag，代表匹配上 Element
        if (this.isTagStart(length).len > 0 || this.isTagEnd(length).len > 0) {
          resolveText()
          return nodes
        } else {
          chars.push(curChar)
          length++;
        }
      } else if (curChar === '$') {
        const res = this.tryParseLatex(length);
        const matchBegin = Math.min(this.cur + length, this.len)
        const matchEnd = Math.min(this.cur + res.matchEnd, this.len)
        if (res.node == null) {
          // 没有匹配上，就向前移动 matchEnd - length + 1 个字符
          chars = chars.concat(this.input.substring(matchBegin, matchEnd))
          length = res.matchEnd
        } else {
          // 碰到 Latex 公式
          resolveText()
          nodes.push(res.node)
          this.consume(0, matchEnd - matchBegin)
        }
      } else {
        chars.push(curChar)
        length++;
      }
    } while (!this.isEof(length));

    resolveText()
    return nodes
  }

  patchNode(node: any) {
    if (node.tag === 'img') {
      const figure = {
        tag: 'figure',
        attrs: {},
        props: {inline: false},
        children: [{tag: node.tag, line: node.line, attrs: node.attrs, props: node.props}] as any[]
      }
      if (node.attrs.alt) {
        figure.children.push({
          tag: 'figcaption', children: [
            {tag: 'text', props: {text: node.attrs.alt}}
          ]
        })
      }
      return figure
    } else {
      return node
    }
  }

  parseElement(matchTag = '') {
    if (!this.accept('<')) return null
    const line = this.line
    const tag = this.parseTag();
    if (tag == null) return null
    this.tags.push({tag, level: this.tagLevel})
    this.consumeSpace();
    // <tag $ 此时忽略 <tag，TOTO 优化为 consumeSpace 含有空格？？
    if (this.look() === '$') return null
    // 拿到属性
    const attrs: Record<string, any> = this.parseAttrs()
    const props: Record<string, any> = {inline: inlineTags.indexOf(tag) !== -1}

    if (this.look() === '/') {
      // 立即封闭标签
      this.accept('/')
      this.accept('>')
      this.tags.pop()
      if (matchTag && tag !== matchTag) {
        return null
      } else {
        return this.patchNode({tag, line, attrs})
      }
    } else {
      if (!this.accept('>')) {
        this.tags.pop()
        return null
      }
      let children: any[] = []
      // 吃掉子节点
      if (tag === 'code') {
        props.text = this.tryMathStr('</code>')
      } else if (tag === 'script' || tag === 'f' || tag === 'fb') {
        props.text = this.tryMathEndTag()
      } else if (tag === 'hr') {

      } else {
        children = this.parseNodes()
      }

      // 吃掉一个闭标签
      const curTag = this.tags.pop()
      const isTagEnd = this.isTagEnd()
      if (isTagEnd.len === 0) {
        console.log(`未匹配的标签(${this.line}): <${curTag?.tag}>...`)
      } else {
        if (curTag?.tag !== isTagEnd.name) {
          const lastTag = this.tags[this.tags.length - 1]
          if (lastTag?.tag === isTagEnd.name) {
            console.log(`匹配上一个标签(${this.line}): <${curTag?.tag}>...</${isTagEnd.name}>`)
          } else {
            console.log(`未匹配的标签(${this.line}): <${curTag?.tag}>...</${isTagEnd.name}>`)
          }
        } else {
          // </name>{光标在此处}
          this.move(3 + isTagEnd.len)
        }
      }

      if (matchTag && tag !== matchTag) {
        return null
      } else {
        if (isClassAttr(tag)) {
          if (attrs.class) {
            attrs.class += ' ' + tag
          } else {
            attrs.class = tag
          }
          return {tag: 'span', attrs, props, children}
        } else {
          return this.patchNode({tag, line, attrs, props, children})
        }
      }
    }
  }

  parseTag() {
    const {name, len} = this.lookName()
    return this.consume(0, len)
  }

  parseAttrs() {
    const attrs: Record<string, any> = {};
    // 1) <hr/>
    // 2) <hr name='123'/>
    // 3) <i>@</i>
    // 4) <i name='123'></i>
    // 5) 错误恢复 <hr <p>111</p>
    while (!this.eof && this.look() !== '$' && this.look() !== '/' && this.look() !== '<' && this.look() !== '>') {
      const name = this.parseTag();
      if (!name) {
        this.accept(this.look());
        continue;
      }

      if (this.look() === '=') {
        this.accept('=');
        this.accept('"');
        const value = this.consumeWhile(/[^"]/);
        this.accept('"');
        attrs[name] = value;
      } else {
        attrs[name] = null;
      }
      this.consumeSpace();
    }
    return transformAttrs(attrs)
  }

  // 匹配到的末尾 [offset, matchEnd)
  tryParseLatex(offset: number = 0): { matchEnd: number, node?: any } {
    let index = offset // 当前字符位置
    let curChar = this.look(offset)
    if (curChar !== '$') return {matchEnd: index}
    // if (this.isEof(offset + 1)) return {matchEnd: offset + 1}
    curChar = this.look(++index)
    let isBlock = curChar === '$'
    // $$ $$，后面至少有三个字符，才能说明这是一个 block
    if (isBlock && this.isEof(index + 3)) {
      // 把 $$ 当做 普通字符
      return {matchEnd: index + 2}
    }

    if (isBlock) {curChar = this.look(++index)}

    let newLineCount = 0
    let hasContent = false
    while (curChar != null && curChar !== '$') {
      if (this.isSpace(curChar)) {
        if (curChar === '\n') {
          newLineCount++
        }
        if (newLineCount > 1) {
          // 里面有空行，不是正常的 latex
          return {matchEnd: index + 1}
        }
      } else {
        // 碰到正常字符，取消(空行) 含多个 '\n' 嫌疑
        newLineCount = 0
        // 碰到标签开始、结束，跳过
        if (this.isTagStart(index).len > 0 || this.isTagEnd(index).len > 0) {
          return {matchEnd: index}
        } else {
          // 其他正常的 ‘书写’ 字符
          hasContent = true
        }
      }
      curChar = this.look(++index)
    }

    if (curChar == null || !hasContent) {
      if (isBlock && this.look(index + 1) === '$') {index++}
      return {matchEnd: index + 1}
    }

    // 匹配上了非空的[offset, length) latex 内容，此时 curChar === '$'
    const latexContent = this.input.substring(this.cur + offset + (isBlock ? 2 : 1), this.cur + index)
    // const consumeOffset = offset + (isBlock ? 2 : 1)
    // const latexContent = this.consume(consumeOffset, index - consumeOffset)
    // 此时 index === '$'
    if (isBlock) {
      if (this.look(index + 1) === '$') index++
      return {
        matchEnd: index + 1,
        node: {tag: 'fb', attrs: {}, props: {text: latexContent, inline: false}}
      }
    } else {
      return {
        matchEnd: index + 1,
        node: {tag: 'f', attrs: {}, props: {text: latexContent, inline: true}}
      }
    }
  }
}

export function parseYedongHtml(html: string, options?: any) {
  const parser = new YedongHtmlParser();
  return parser.parse(html, options);
}
